﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.Mvc;

namespace WinnerCMS.WebUI
{
    public enum CachePolicy
    {
        NoCache = 0,
        Client = 1,
        Server = 2,
        ClientAndServer = 3
    }

    /// <summary>
    /// [OutputCache(Duration = 60, VaryByParam = "*", CachePolicy = CachePolicy.Server)]
    /// [OutputCache(Duration = 10, VaryByParam = "id", CachePolicy = CachePolicy.Server)]
    /// [OutputCache(Duration = 10, VaryByParam = "id;page", CachePolicy = CachePolicy.Server)]
    /// [OutputCache(Duration = 10, CachePolicy = CachePolicy.Client)]
    /// </summary>
    public class OutputCache : ActionFilterAttribute
    {
        #region Public properties

        public int Duration { get; set; }
        public string VaryByParam { get; set; }
        public CachePolicy CachePolicy { get; set; }

        #endregion

        #region Private members

        private Cache cache = null;
        private bool cacheHit = false;
        private HttpContext existingContext;
        private StringWriter writer;

        #endregion

        #region ActionFilterAttribute overrides

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            // Client-side caching?
            if (CachePolicy == CachePolicy.Client || CachePolicy == CachePolicy.ClientAndServer)
            {
                if (Duration <= 0) return;

                HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
                TimeSpan cacheDuration = TimeSpan.FromSeconds(Duration);

                cache.SetCacheability(HttpCacheability.Public);
                cache.SetExpires(DateTime.Now.Add(cacheDuration));
                cache.SetMaxAge(cacheDuration);
                cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
            }
        }

        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            // Server-side caching?
            if (CachePolicy == CachePolicy.Server || CachePolicy == CachePolicy.ClientAndServer)
            {
                // Fetch Cache instance
                this.cache = filterContext.HttpContext.Cache;

                // Fetch cached data
                object cachedData = this.cache.Get(GenerateKey(filterContext));
                if (cachedData != null)
                {
                    // Cache hit! Return cached data
                    this.cacheHit = true;
                    filterContext.HttpContext.Response.Write(cachedData);
                    filterContext.Cancel = true;
                }
                else
                {
                    // Cache not hit.
                    // Replace the current context with a new context that writes to a string writer
                    this.existingContext = System.Web.HttpContext.Current;
                    this.writer = new StringWriter();
                    HttpResponse response = new HttpResponse(this.writer);
                    HttpContext context = new HttpContext(this.existingContext.Request, response)
                    {
                        User = this.existingContext.User
                    };

                    // Copy all items in the context (especially done for session availability in the component)
                    foreach (var key in this.existingContext.Items.Keys)
                    {
                        context.Items[key] = this.existingContext.Items[key];
                    }

                    System.Web.HttpContext.Current = context;
                }
            }
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            // Server-side caching?
            if (CachePolicy == CachePolicy.Server || CachePolicy == CachePolicy.ClientAndServer)
            {
                if (!this.cacheHit)
                {
                    // Restore the old context
                    System.Web.HttpContext.Current = this.existingContext;

                    // Return rendererd data
                    this.existingContext.Response.Write(this.writer.ToString());

                    // Add data to cache
                    this.cache.Add(
                        GenerateKey(filterContext),
                        this.writer.ToString(),
                        null,
                        DateTime.Now.AddSeconds(Duration),
                        Cache.NoSlidingExpiration,
                        CacheItemPriority.Normal,
                        null);
                }
            }
        }

        #endregion

        #region Helper methods

        private string GenerateKey(ControllerContext filterContext)
        {
            StringBuilder cacheKey = new StringBuilder();

            // Controller + action
            cacheKey.Append(filterContext.Controller.GetType().FullName);
            if (filterContext.RouteData.Values.ContainsKey("action"))
            {
                cacheKey.Append("_");
                cacheKey.Append(filterContext.RouteData.Values["action"].ToString());
            }

            // Variation by parameters
            List<string> varyByParam = VaryByParam.Split(';').ToList();

            if (!string.IsNullOrEmpty(VaryByParam))
            {
                foreach (KeyValuePair<string, object> pair in filterContext.RouteData.Values)
                {
                    if (VaryByParam == "*" || varyByParam.Contains(pair.Key))
                    {
                        cacheKey.Append("_");
                        cacheKey.Append(pair.Key);
                        cacheKey.Append("=");
                        cacheKey.Append(pair.Value.ToString());
                    }
                }
            }

            return cacheKey.ToString();
        }

        #endregion
    }
}